package ah.classfinder;

import java.awt.BorderLayout;
import java.awt.FlowLayout;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTabbedPane;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.UIManager;
import javax.swing.border.EtchedBorder;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.filechooser.FileFilter;
import javax.swing.table.DefaultTableModel;

public class MainFrame extends JFrame implements ActionListener,ItemListener {

    static class Config implements Serializable{
        
        private static final long serialVersionUID = 1L;
        File lastDirectory;
        boolean isIncludePackage=true;
        boolean isCaseSensitive=false;
        String lastText;
    }
    
    static class FileInfo implements Serializable {

        private static final long serialVersionUID = 1L;
        File jarFile;
        //
        int classCount = 0;
        String[] classNames;
        long lastModifiedTime = 0;
        //
        public FileInfo(File jarFile) {
            this.jarFile = jarFile;
        }

        void build() {
            if (jarFile == null) return;
            if (lastModifiedTime == jarFile.lastModified()) return;
            lastModifiedTime = jarFile.lastModified();
            try {
                List<JarEntry> list = Collections.list(new JarFile(jarFile).entries());
                List<String> fullNames = new ArrayList<String>();
                for (JarEntry e : list) {
                    String n = e.getName();
                    if (n.endsWith(".class")) {
                        n=n.substring(0,n.length()-6);
                        fullNames.add(n.replace('/', '.'));
                    }
                }
                classCount = fullNames.size();
                classNames = fullNames.toArray(new String[classCount]);
            } catch (Exception e) {
                System.err.println(e.getMessage());
            }
            //System.out.println(toString());
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) return true;
            if (obj == null) return false;
            if (getClass() != obj.getClass()) return false;
            FileInfo other = (FileInfo) obj;
            if (jarFile == null) {
                if (other.jarFile != null) return false;
            } else if (!jarFile.equals(other.jarFile)) return false;
            return true;
        }

        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + ((jarFile == null) ? 0 : jarFile.hashCode());
            return result;
        }

        public String[] toRow() {
            return new String[] { jarFile.getName(), "" + classCount, jarFile.getAbsolutePath() };
        }

        @Override
        public String toString() {
            return "FileInfo [jarFile=" + jarFile + ", lastModifiedTime=" + lastModifiedTime
                    + ", classCount=" + classCount + "]";
        }

    }

    class MyTableModel extends DefaultTableModel {
        private static final long serialVersionUID = 1L;

        public MyTableModel(Object[] columnNames, int rowCount) {
            super(columnNames, rowCount);
        }

        void clear() {
            int count = dataVector.size();
            if (count > 0) {
                dataVector.clear();
                fireTableRowsDeleted(0, count - 1);
            }
        }
    }

    private static final long serialVersionUID = 1L;

    @SuppressWarnings("unchecked")
    public static void main(String[] args) throws Exception {
        UIManager.put("swing.boldMetal", Boolean.FALSE);
        //
        MainFrame.config=(Config)readObject(new File(System.getProperty("user.home", ".") + "/cf.conf"));
        if(MainFrame.config==null)
            MainFrame.config=new Config();
        //
        MainFrame mf = new MainFrame();
        
        File f = new File(System.getProperty("user.home", ".") + "/cf.jarhistory");
        List<FileInfo> fis=(List<FileInfo>)readObject(f);
        fis=fis!=null?fis:new ArrayList<FileInfo>();
        mf.fileInfos =fis;
        mf.buildMap(true);
        //
        //
        mf.cnameText.setText(MainFrame.config.lastText);
        mf.statuLabel[3].setText("v0.1 20090813 (xylz@live.cn)");
        mf.setVisible(true);
        //print(new File("D:/workspace/xiaonei/xiaonei-trunk/trunk/www/WEB-INF/lib/antlr-2.7.6.jar"));
    }

    /**
     * 匹配字符串
     * 
     * @param pattern 表达式
     * @param str 字符串
     * @param isCaseSensitive 是否匹配大小写，如果是false则忽略大小写
     * @return 匹配的结果
     */
    public static boolean match(String pattern, String str, boolean isCaseSensitive) {
        char[] patArr = pattern.toCharArray();
        char[] strArr = str.toCharArray();
        int patIdxStart = 0;
        int patIdxEnd = patArr.length - 1;
        int strIdxStart = 0;
        int strIdxEnd = strArr.length - 1;
        char ch;

        boolean containsStar = false;
        for (int i = 0; i < patArr.length; i++) {
            if (patArr[i] == '*') {
                containsStar = true;
                break;
            }
        }

        if (!containsStar) {
            // No '*'s, so we make a shortcut
            if (patIdxEnd != strIdxEnd) {
                return false; // Pattern and string do not have the same size
            }
            for (int i = 0; i <= patIdxEnd; i++) {
                ch = patArr[i];
                if (ch != '?') {
                    if (isCaseSensitive && ch != strArr[i]) {
                        return false; // Character mismatch
                    }
                    if (!isCaseSensitive
                            && Character.toUpperCase(ch) != Character.toUpperCase(strArr[i])) {
                        return false; // Character mismatch
                    }
                }
            }
            return true; // String matches against pattern
        }

        if (patIdxEnd == 0) {
            return true; // Pattern contains only '*', which matches anything
        }

        // Process characters before first star
        while ((ch = patArr[patIdxStart]) != '*' && strIdxStart <= strIdxEnd) {
            if (ch != '?') {
                if (isCaseSensitive && ch != strArr[strIdxStart]) {
                    return false; // Character mismatch
                }
                if (!isCaseSensitive
                        && Character.toUpperCase(ch) != Character.toUpperCase(strArr[strIdxStart])) {
                    return false; // Character mismatch
                }
            }
            patIdxStart++;
            strIdxStart++;
        }
        if (strIdxStart > strIdxEnd) {
            // All characters in the string are used. Check if only '*'s are
            // left in the pattern. If so, we succeeded. Otherwise failure.
            for (int i = patIdxStart; i <= patIdxEnd; i++) {
                if (patArr[i] != '*') {
                    return false;
                }
            }
            return true;
        }

        // Process characters after last star
        while ((ch = patArr[patIdxEnd]) != '*' && strIdxStart <= strIdxEnd) {
            if (ch != '?') {
                if (isCaseSensitive && ch != strArr[strIdxEnd]) {
                    return false; // Character mismatch
                }
                if (!isCaseSensitive
                        && Character.toUpperCase(ch) != Character.toUpperCase(strArr[strIdxEnd])) {
                    return false; // Character mismatch
                }
            }
            patIdxEnd--;
            strIdxEnd--;
        }
        if (strIdxStart > strIdxEnd) {
            // All characters in the string are used. Check if only '*'s are
            // left in the pattern. If so, we succeeded. Otherwise failure.
            for (int i = patIdxStart; i <= patIdxEnd; i++) {
                if (patArr[i] != '*') {
                    return false;
                }
            }
            return true;
        }

        // process pattern between stars. padIdxStart and patIdxEnd point
        // always to a '*'.
        while (patIdxStart != patIdxEnd && strIdxStart <= strIdxEnd) {
            int patIdxTmp = -1;
            for (int i = patIdxStart + 1; i <= patIdxEnd; i++) {
                if (patArr[i] == '*') {
                    patIdxTmp = i;
                    break;
                }
            }
            if (patIdxTmp == patIdxStart + 1) {
                // Two stars next to each other, skip the first one.
                patIdxStart++;
                continue;
            }
            // Find the pattern between padIdxStart & padIdxTmp in str between
            // strIdxStart & strIdxEnd
            int patLength = (patIdxTmp - patIdxStart - 1);
            int strLength = (strIdxEnd - strIdxStart + 1);
            int foundIdx = -1;
            strLoop: for (int i = 0; i <= strLength - patLength; i++) {
                for (int j = 0; j < patLength; j++) {
                    ch = patArr[patIdxStart + j + 1];
                    if (ch != '?') {
                        if (isCaseSensitive && ch != strArr[strIdxStart + i + j]) {
                            continue strLoop;
                        }
                        if (!isCaseSensitive
                                && Character.toUpperCase(ch) != Character
                                        .toUpperCase(strArr[strIdxStart + i + j])) {
                            continue strLoop;
                        }
                    }
                }

                foundIdx = strIdxStart + i;
                break;
            }

            if (foundIdx == -1) {
                return false;
            }

            patIdxStart = patIdxTmp;
            strIdxStart = foundIdx + patLength;
        }

        // All characters in the string are used. Check if only '*'s are left
        // in the pattern. If so, we succeeded. Otherwise failure.
        for (int i = patIdxStart; i <= patIdxEnd; i++) {
            if (patArr[i] != '*') {
                return false;
            }
        }
        return true;
    }

    static void print(File jarFile) throws Exception {
        JarFile file = new JarFile(jarFile);
        for (JarEntry e : Collections.list(file.entries())) {
            System.out.println(e.getName());
        }
    }

    String[] actions = { "Add", "Delete" };

    JButton[] buttons = new JButton[actions.length];

    JCheckBox caseSensitive=new JCheckBox("CaseSensitive");

    Map<String, FileInfo> classes = new HashMap<String, FileInfo>();

    private boolean dataChanged = false;
    List<FileInfo> fileInfos = new ArrayList<FileInfo>();
    
    JCheckBox includePackage=new JCheckBox("IncludePackage");
    
    JTabbedPane jtp = new JTabbedPane();
    long lastSearchTime = 0;


    Map<String, FileInfo> shotClasses = new HashMap<String, FileInfo>();

    JLabel[] statuLabel = new JLabel[4];

    boolean stopLast = false;
    static Config config =new Config();

    JTable[] ts = new JTable[2];
    final JTextField cnameText = new JTextField();
    public MainFrame() {
        super("Class Finder");
        setLayout(new BorderLayout());
        setSize(800,600);
        //
        JPanel titlePanel = new JPanel(new BorderLayout());
        
        cnameText.setColumns(120);
        cnameText.addFocusListener(new FocusAdapter() {
            @Override
            public void focusGained(FocusEvent e) {
             if(cnameText.getText().length()>0) {
                 cnameText.setSelectionStart(0);
                 cnameText.setSelectionEnd(cnameText.getText().length());
             }
            }
        });
        cnameText.getDocument().addDocumentListener(new DocumentListener() {

            @Override
            public void changedUpdate(DocumentEvent e) {
                doText(cnameText.getText());
            }

            @Override
            public void insertUpdate(DocumentEvent e) {
                doText(cnameText.getText());
            }

            @Override
            public void removeUpdate(DocumentEvent e) {
                doText(cnameText.getText());
            }
        });
        titlePanel.add(cnameText, BorderLayout.NORTH);
        JPanel buttonPanel = new JPanel(new FlowLayout());
        for (int i = 0; i < buttons.length; i++) {
            buttons[i] = new JButton(actions[i]);
            buttons[i].setActionCommand(actions[i]);
            buttons[i].addActionListener(this);
            buttonPanel.add(buttons[i]);
        }
        includePackage.setSelected(config.isIncludePackage);
        caseSensitive.setSelected(config.isCaseSensitive);
        includePackage.setActionCommand("IncludePackage");
        caseSensitive.setActionCommand("CaseSensitive");
        includePackage.addItemListener(this);
        caseSensitive.addItemListener(this);
        buttonPanel.add(includePackage);
        buttonPanel.add(caseSensitive);
        titlePanel.add(buttonPanel, BorderLayout.SOUTH);
        //
        getContentPane().add(titlePanel, BorderLayout.NORTH);
        //

        //
        {
            MyTableModel fileModel = new MyTableModel(new String[] { "FileName", "Count",
                    "FullPath" }, 0);
            JTable table = new JTable(fileModel);
            table.getColumnModel().getColumn(0).setPreferredWidth(160);
            table.getColumnModel().getColumn(1).setPreferredWidth(20);
            table.getColumnModel().getColumn(2).setPreferredWidth(400);
            table.setDragEnabled(false);
            table.setAutoResizeMode(JTable.AUTO_RESIZE_NEXT_COLUMN);

            //
            // fileModel.addRow(new FileInfo(new File("D:/workspace/xiaonei/xiaonei-trunk/trunk/www/WEB-INF/lib/antlr-2.7.6.jar")).toRow());
            //
            JScrollPane jsp = new JScrollPane(table);
            jsp.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
            jsp.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
            //

            jtp.add("FileList", jsp);
            ts[0] = table;
        }
        //
        {
            MyTableModel dtm = new MyTableModel(new String[] { "ClassName", "JarFile", "Open" }, 0);
            JTable table = new JTable(dtm);
            table.getColumnModel().getColumn(0).setPreferredWidth(80);
            table.getColumnModel().getColumn(1).setPreferredWidth(500);
            table.getColumnModel().getColumn(2).setPreferredWidth(20);
            table.setDragEnabled(false);
            table.setAutoResizeMode(JTable.AUTO_RESIZE_NEXT_COLUMN);

            JScrollPane jsp = new JScrollPane(table);
            jsp.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
            jsp.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
            //

            jtp.add("ClassList", jsp);
            ts[1] = table;
        }
        //
        getContentPane().add(jtp, BorderLayout.CENTER);
        //
        JPanel bottomPanel = new JPanel(new GridLayout(1, statuLabel.length));
        for (int i = 0; i < statuLabel.length; i++) {
            statuLabel[i] = new JLabel();
            statuLabel[i].setBorder(BorderFactory.createEtchedBorder(EtchedBorder.LOWERED));
            bottomPanel.add(statuLabel[i]);
        }
        //
        jtp.addChangeListener(new ChangeListener() {

            @Override
            public void stateChanged(ChangeEvent e) {
                doButtonState();
            }
        });
        //
        getContentPane().add(bottomPanel, BorderLayout.SOUTH);
        //
        addWindowListener(new WindowAdapter() {

            @Override
            public void windowClosing(WindowEvent e) {
                exit();
            }
        });

        doButtonState();
        //
    }
    
    @Override
    public void actionPerformed(ActionEvent e) {
        String cmd = e.getActionCommand();
        if ("Add".equals(cmd)) {
            JFileChooser jfc = new JFileChooser(config.lastDirectory);
            jfc.setFileSelectionMode(JFileChooser.FILES_ONLY);
            jfc.setMultiSelectionEnabled(true);
            jfc.setAcceptAllFileFilterUsed(false);
            jfc.setFileFilter(new FileFilter() {

                public boolean accept(File f) {
                    String n = f.getName();
                    return f.isDirectory() || n.endsWith(".jar") || n.endsWith(".zip")
                            || n.endsWith(".war");
                }

                public String getDescription() {
                    return "Zip File(.jar,.zip,war)";
                }
            });
            if (JFileChooser.APPROVE_OPTION == jfc.showOpenDialog(this)) {
                config.lastDirectory = jfc.getSelectedFiles()[0].getParentFile();
                dataChanged = true;
                for (File f : jfc.getSelectedFiles()) {
                    FileInfo info = new FileInfo(f);
                    fileInfos.remove(info);
                    fileInfos.add(info);
                }
                buildMap(true);
            }
        } else if ("Delete".equals(cmd)) {
            if (ts[0].getSelectedRowCount() > 0) {
                int[] si = ts[0].getSelectedRows();
                Arrays.sort(si);
                dataChanged = true;
                for (int i = si.length - 1; i >= 0; i--) {
                    String path = (String) ts[0].getValueAt(si[i], 2);
                    fileInfos.remove(new FileInfo(new File(path)));
                    ((DefaultTableModel) ts[0].getModel()).removeRow(si[i]);

                }
                buildMap(false);
            }
        }
    }

    void addRow(int index, String[] row) {
        ((DefaultTableModel) ts[index].getModel()).addRow(row);
        ts[index].validate();
    }

    void buildMap(boolean addRow) {
        classes.clear();
        shotClasses.clear();
        if (addRow) ((MyTableModel) ts[0].getModel()).clear();
        for (FileInfo in : fileInfos) {
            in.build();
            if (addRow) addRow(0, in.toRow());
            for (String s : in.classNames) {
                classes.put(s, in);
                int i=s.lastIndexOf('.');
                if(i>-1)s=s.substring(i+1);
                shotClasses.put(s, in);
            }
        }
        statuLabel[1].setText("Class/File: " + classes.size() + "/" + fileInfos.size());
    }

    protected void doButtonState() {
        boolean b = jtp.getSelectedIndex() == 0;
        buttons[0].setEnabled(b);
        buttons[1].setEnabled(b);
        caseSensitive.setEnabled(!b);
        includePackage.setEnabled(!b);

    }

    void doText(String s) {
        if (s == null || s.length() < 2) return;
        if ('*' != s.charAt(0)) s = "*" + s;
        if ('*' != s.charAt(s.length() - 1)) s = s + "*";
        if (System.currentTimeMillis() - lastSearchTime < 500L) return;
        //
        config.lastText=s;
        if(jtp.getSelectedIndex()!=1)
            jtp.setSelectedIndex(1);
        //
        MyTableModel dtm = ((MyTableModel) ts[1].getModel());
        if(ts[1].getSelectedRowCount()>0)
            ts[1].clearSelection();
        dtm.clear();
        for (Map.Entry<String, FileInfo> e : config.isIncludePackage?classes.entrySet():shotClasses.entrySet()) {
            if (stopLast) return;
            if (match(s, e.getKey(), config.isCaseSensitive)) {
                dtm.addRow(new String[] { e.getKey(), e.getValue().jarFile.getAbsolutePath(), "" });
            }
        }
        statuLabel[2].setText("Find Class: " + ts[1].getRowCount());
    }

    static Object  readObject(File f) {
        if(f==null||!f.exists()||!f.isFile())return null;
        try {
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream(f));
            Object o=ois.readObject();
            ois.close();
            return o;
        } catch (Exception e) {
            return null;
        }
    }
    
    static void writeObject(Object obj,File f) {
        try {
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(f));
            oos.writeObject(obj);
            oos.close();
        } catch (Exception e) {
        }
    }
    
    void exit() {
        if (dataChanged) {
            File f = new File(System.getProperty("user.home", ".") + "/cf.jarhistory");
            writeObject(fileInfos, f);
        }
        writeObject(config , new File(System.getProperty("user.home", ".") + "/cf.conf"));
        System.exit(0);
    }

    @Override
    public void itemStateChanged(ItemEvent e) {
        if(e.getSource()==includePackage||e.getSource()==caseSensitive) {
           //
           //if(!includePackage.isEnabled())return;
            config.isIncludePackage=includePackage.isSelected();
            config.isCaseSensitive=caseSensitive.isSelected();
           doText(config.lastText);
        }
    }
}
